/******************************************************************************* * Copyright (c) 2012, 2013 Torkild U. Resheim. * * All rights reserved. This program and the accompanying materials are made * available under the terms of the Eclipse Public License v1.0 which * accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Torkild U. Resheim - initial API and implementation *******************************************************************************/ package no.resheim.elibrarium.library.core; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.sql.SQLException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import no.resheim.elibrarium.library.Book; import no.resheim.elibrarium.library.Library; import no.resheim.elibrarium.library.core.ILibraryCatalog.ITransactionalOperation; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.ISafeRunnable; import org.eclipse.core.runtime.ListenerList; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.Plugin; import org.eclipse.core.runtime.SafeRunner; import org.eclipse.emf.cdo.server.CDOServerUtil; import org.eclipse.emf.cdo.server.IRepository; import org.eclipse.emf.cdo.server.IStore; import org.eclipse.emf.cdo.server.db.CDODBUtil; import org.eclipse.emf.cdo.server.db.mapping.IMappingStrategy; import org.eclipse.emf.cdo.server.net4j.CDONet4jServerUtil; import org.eclipse.emf.common.util.EList; import org.eclipse.net4j.acceptor.IAcceptor; import org.eclipse.net4j.db.DBUtil; import org.eclipse.net4j.db.IDBAdapter; import org.eclipse.net4j.db.IDBConnectionProvider; import org.eclipse.net4j.db.h2.H2Adapter; import org.eclipse.net4j.util.container.IPluginContainer; import org.eclipse.net4j.util.lifecycle.LifecycleUtil; import org.eclipse.net4j.util.om.OMBundle; import org.eclipse.net4j.util.om.OMPlatform; import org.eclipse.net4j.util.om.log.OMLogger; import org.eclipse.net4j.util.om.trace.OMTracer; import org.h2.jdbcx.JdbcDataSource; import org.h2.tools.Server; import org.osgi.framework.BundleContext; public class Librarian extends Plugin implements ILibrarian { /** Identifier of this plug-in */ public static final String PLUGIN_ID = "no.resheim.elibrarium.library.core"; /** The default CDO server port */ private static int serverPort = 1409; public static final OMBundle BUNDLE = OMPlatform.INSTANCE.bundle(PLUGIN_ID, Librarian.class); /** Identifier of the CDO repository used */ public static final String CDO_REPOSITORY_ID = "elibrarium"; private static IAcceptor acceptor; public static final OMTracer DEBUG = BUNDLE.tracer("debug"); //$NON-NLS-1$ public static final OMLogger LOG = BUNDLE.logger(); private static Librarian plugin; private static IRepository repository; /** * Returns the shared instance * * @return the shared instance */ public static Librarian getDefault() { return plugin; } public static int getServerPort() { return serverPort; } /** * Returns port of the HTTP based database console. */ public static int getConsolePort() { return getServerPort() + 1; } private final ArrayList<ICollection> collection; private final ListenerList listeners; private String storageLocation = null; public Librarian() { collection = new ArrayList<ICollection>(); listeners = new ListenerList(); plugin = this; } @Override public synchronized void addBook(final Book book) { // Create a new transaction for modifying the library ITransactionalOperation<Library> operation = new ITransactionalOperation<Library>() { @Override public Object execute(Library object) { object.cdoWriteLock().lock(); EList<Book> books = object.getBooks(); books.add(book); object.cdoWriteLock().unlock(); return null; } }; // Execute the transaction ILibraryCatalog.INSTANCE.modify(ILibraryCatalog.INSTANCE.getLibrary(), operation); // Notify listeners about the change Object[] l = listeners.getListeners(); for (int i = 0; i < l.length; i++) { final ILibraryListener listener = (ILibraryListener) l[i]; SafeRunner.run(new ISafeRunnable() { @Override public void handleException(Throwable exception) { exception.printStackTrace(); } @Override public void run() throws Exception { listener.bookAdded(book); } }); } } public void addCollection(ICollection provider) { collection.add(provider); provider.addListener(this); } public void addListener(ILibraryListener listener) { listeners.add(listener); } private void configure() { File user = new File(System.getProperties().getProperty("user.dir") + File.separator + ".elibrarium"); File home = new File(System.getProperties().getProperty("user.home") + File.separator + ".elibrarium"); try { if (user.exists()) { readConfiguration(user); } else if (home.exists()) { readConfiguration(home); } } catch (IOException e) { } if (storageLocation == null) { String root = System.getProperty("user.home"); String os = System.getProperty("os.name").toLowerCase(); if (os.indexOf("mac") > -1) { root = root + File.separator + "Library" + File.separator; } storageLocation = new File(root + File.separator + "Elibrarium").getAbsolutePath(); } } /** * Start the h2 database server. * * @throws Exception * @see {@link #getStorageLocation()} */ protected void doStart() throws Exception { LOG.info("Elibrarium server starting"); JdbcDataSource dataSource = new JdbcDataSource(); // XXX: This is a workaround for not handling inter-process transactions // through CDO. It also solves several other problems, such as what // happens when the database server dies. // // http://h2database.com/html/features.html#auto_mixed_mode // String url = "jdbc:h2:" + getStorageLocation() + File.separator + // "h2db;AUTO_SERVER=TRUE"; String url = "jdbc:h2:" + getStorageLocation() + File.separator + "h2db;AUTO_SERVER=TRUE"; LOG.info("- Hibernate database URL = " + url); dataSource.setURL(url); // Create one database table per concrete model class IMappingStrategy mappingStrategy = CDODBUtil.createHorizontalMappingStrategy(true); IDBAdapter dbAdapter = new H2Adapter(); IDBConnectionProvider dbConnectionProvider = DBUtil.createConnectionProvider(dataSource); IStore store = CDODBUtil.createStore(mappingStrategy, dbAdapter, dbConnectionProvider); Map<String, String> props = new HashMap<String, String>(); props.put(IRepository.Props.OVERRIDE_UUID, CDO_REPOSITORY_ID); props.put(IRepository.Props.SUPPORTING_AUDITS, "true"); props.put(IRepository.Props.SUPPORTING_BRANCHES, "false"); repository = CDOServerUtil.createRepository(CDO_REPOSITORY_ID, store, props); CDOServerUtil.addRepository(IPluginContainer.INSTANCE, repository); CDONet4jServerUtil.prepareContainer(IPluginContainer.INSTANCE); acceptor = (IAcceptor) IPluginContainer.INSTANCE.getElement("org.eclipse.net4j.acceptors", "tcp", getDbServerAddress()); LOG.info("- CDO acceptor started at " + getDbServerAddress()); startConsole(); LOG.info("Elibrarium server started"); } /** * Returns the TCP address of the H2 server. */ public static String getDbServerAddress() { return "127.0.0.1:" + getServerPort(); } /** * Stop the CDO persistence server. * * @throws Exception */ protected void doStop() throws Exception { LOG.info("Elibrarium stopping"); LifecycleUtil.deactivate(repository); LifecycleUtil.deactivate(acceptor); LOG.info("Elibrarium stopped"); } /** * Returns the identified by the external identifier. * * @param urn * the book identifier * @return a list of books * @since 1.0 */ public synchronized Book getBookByURN(String urn) { EList<Book> items = ILibraryCatalog.INSTANCE.getLibrary().getBooks(); for (Book item : items) { if (item.getBookURN().equals(urn)) { return item; } } return null; } /** * Returns a list of all books found in the collection with the given * identifier. * * @param id * the collection identifier * @return a list of books * @since 1.0 */ public synchronized List<Book> getBooksByCollection(String id) { ArrayList<Book> books = new ArrayList<Book>(); EList<Book> items = ILibraryCatalog.INSTANCE.getLibrary().getBooks(); for (Book item : items) { if (item.getCollection().equals(id)) { books.add(item); } } return books; } /** * * @return the shared library instance * @since 1.0 */ public synchronized Library getLibrary() { return ILibraryCatalog.INSTANCE.getLibrary(); } /** * Returns the root folder of where Elibrarium stores it's data files. On OS * X this is normally <i>~/Library/Elibrarium</i>. On other platforms it is * normally at ~/Elibrarium. * * @return the path to the storage location. * @throws IOException */ public IPath getStorageLocation() { return new Path(storageLocation); } private void readConfiguration(File file) throws FileNotFoundException, IOException { Properties p = new Properties(); p.load(new FileReader(file)); storageLocation = p.getProperty("storage.location"); serverPort = Integer.parseInt(p.getProperty("server.port")); LOG.info("Using configuration file at " + file.getAbsolutePath()); LOG.info("- storage.location = " + storageLocation); LOG.info("- server.port=" + serverPort); } @Override public synchronized void removeBook(final Book book) { // Create a new transaction for modifying the library ITransactionalOperation<Library> operation = new ITransactionalOperation<Library>() { @Override public Object execute(Library object) { object.cdoWriteLock().lock(); EList<Book> books = object.getBooks(); books.remove(book); object.cdoWriteLock().unlock(); return null; } }; // Execute the transaction ILibraryCatalog.INSTANCE.modify(ILibraryCatalog.INSTANCE.getLibrary(), operation); Object[] l = listeners.getListeners(); for (int i = 0; i < l.length; i++) { final ILibraryListener listener = (ILibraryListener) l[i]; SafeRunner.run(new ISafeRunnable() { @Override public void handleException(Throwable exception) { exception.printStackTrace(); } @Override public void run() throws Exception { listener.bookRemoved(book); } }); } } public void removeListener(ILibraryListener listener) { listeners.remove(listener); } @Override public void start(BundleContext context) throws Exception { super.start(context); configure(); doStart(); CdoLibraryCatalog.INSTANCE.activate(); } /** * Start a h2 web-based console for debugging and data browsing. */ private void startConsole() { try { Server.createWebServer(new String[] { "-webPort", String.valueOf(getConsolePort()) }).start(); LOG.info("- Started h2 console at http://127.0.0.1:" + getConsolePort()); } catch (SQLException e) { LOG.info("- Could not start h2 console"); } } @Override public void stop(BundleContext context) throws Exception { super.stop(context); doStop(); CdoLibraryCatalog.INSTANCE.deactivate(); } }